#!/usr/bin/env python3
import argparse
import os
import sys
import yaml
import logging
from pathlib import Path


EXIT_INVALID_PATH = 1
EXIT_INCONSISTENCY = 2
DECO_SPLIT = '=' * 30

logger = None
LOG_LEVEL = {
    'DEBUG': logging.DEBUG,
    'INFO': logging.INFO,
    'WARNING': logging.WARNING,
    'ERROR': logging.ERROR
}
DEFAULT_LOG_LEVEL = 'INFO'
IS_DEBUG = False
ENV_LOG_LEVEL = 'LOG_LEVEL'


def log_debug(log_str):
    if IS_DEBUG:
        logger.warning(log_str)


def parse_conan_entry(entry):
    parts = entry.split('/')
    if len(parts) < 2:
        return None, None
    name = parts[0]
    version = parts[1].split('@')[0]
    return name, version


def setup_logging():
    log_level = os.getenv(ENV_LOG_LEVEL)

    if log_level not in LOG_LEVEL:
        log_level = DEFAULT_LOG_LEVEL

    global logger
    global IS_DEBUG
    IS_DEBUG = log_level == 'DEBUG'
    logger = logging.getLogger('VerConsistCheck')
    logger.setLevel(LOG_LEVEL[log_level])
    formatter = logging.Formatter('[%(name)s] - [%(levelname)s] - %(message)s')

    console_handler = logging.StreamHandler()
    console_handler.setLevel(LOG_LEVEL[log_level])
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)


def get_common_files(dir_files):
    all_files = set().union(*dir_files.values())
    common_files = set(all_files)
    for files in dir_files.values():
        common_files.intersection_update(files)
    return common_files


def check_uncommon_files(dir_files):
    common_files = get_common_files(dir_files)

    uncommon_files_report = {}
    for d, files in dir_files.items():
        uncommon_files = files - common_files
        for uf in uncommon_files:
            uncommon_files_report.setdefault(uf, []).append(d)

    return uncommon_files_report


def check_uncommon_names(dir_files):
    common_files = get_common_files(dir_files)

    uncommon_app_report = {}
    for file in common_files:
        file_entries = {}
        for d in dir_files:
            entries = {}
            try:
                with open(d.joinpath(file), 'r') as f:
                    data = yaml.safe_load(f)
                    for item in data.get('dependencies', []):
                        comp = item.get('conan', None)
                        name, version = parse_conan_entry(comp)
                        entries[name] = version
            except Exception as e:
                logger.error(f"Yaml error in {d.joinpath(file)}:{e}!")
            file_entries[d] = entries
        
        all_apps = set().union(*[e.keys() for e in file_entries.values()])

        for app in all_apps:
            present_in_all = True
            versions = {}
            for d in dir_files:
                if app not in file_entries[d]:
                    present_in_all = False
                    versions[d] = None
                else:
                    versions[d] = file_entries[d][app]

            if not present_in_all:
                present_dirs = []
                missing_dirs = []
                for d in dir_files:
                    if app in file_entries[d]:
                        present_dirs.append(d.name)
                    else:
                        missing_dirs.append(d.name)
                uncommon_app_report[app] = {d: file for d in present_dirs}
            else:
                unique_versions = set(versions.values())
                if len(unique_versions) > 1:
                    version_info = {d.name: ver for d, ver in versions.items()}
                    uncommon_app_report[f"{app}({file})"] = version_info

    return uncommon_app_report


def generate_table(headers, data):
    col_widths = [
        max(len(str(row[i])) for row in [headers] + data)
        for i in range(len(headers))
    ]

    header_line = "| " + " | ".join(
        header.ljust(width) for header, width in zip(headers, col_widths)
    ) + " |"

    spar_line = "+-" + "-+-".join(
        '-' * width for width in col_widths
    ) + "-+"

    title_line = spar_line.replace('-', '=')

    data_lines = []
    for row in data:
        data_line = "| " + " | ".join(
            str(cell).ljust(width) for cell, width in zip(row, col_widths)
        ) + " |"
        data_lines.append(data_line)
        data_lines.append(spar_line)

    logger.error('\n' + '\n'.join([spar_line, header_line, title_line] + data_lines))


def main():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    group = parser.add_mutually_exclusive_group(required=True)

    group.add_argument(
        "-s",
        "--subsys-path",
        type=Path,
        default="./build/subsys",
        help="提供要检查版本同一性的根目录"
    )
    group.add_argument(
        "-f",
        "--folders",
        nargs="+",
        help="需要检查版本同一性的文件路径列表（空格分隔）"
    )
    args = parser.parse_args()

    setup_logging()

    if args.subsys_path.exists() and args.subsys_path.is_dir():
        subpaths = [f.expanduser().resolve() for f in args.subsys_path.iterdir() if f.is_dir()]
    else:
        subpaths = list(map(lambda p: Path(p).expanduser().resolve(), args.folders))

    dir_files = {}
    for path in subpaths:
        if not path.exists() or not path.is_dir():
            raise ValueError(f"非法路径：{path}")
        for item in path.rglob("*.yml"):
            dir_files.setdefault(path, set()).add(item.name)

    file_check = check_uncommon_files(dir_files)
    app_check = check_uncommon_names(dir_files)

    if file_check or app_check:
        logger.error("Inconsistency configs exist!")

        headers = [""]
        data = {}
        for dir_name in dir_files:
            headers.append(dir_name.name)
            for uncommon_file, presents in file_check.items():
                data.setdefault(uncommon_file, []).append("√" if dir_name in presents else "×")

            for uncommon_app, reason in app_check.items():
                data.setdefault(uncommon_app, []).append(reason.get(dir_name.name, "×"))
        generate_table(headers, [[k] + v for k, v in data.items()])

        sys.exit(EXIT_INCONSISTENCY)

    logger.info("Pass Consistency Check!")

            
if __name__ == "__main__":
    main()
